Kong 脚本学习
基本插件结构
simple-plugin
├── handler.lua
└── schema.lua
- handler.lua 插件的核心,可以定义一些在请求的各个声明周期运行的函数。
- schema.lua 插件的一些配置信息。比如可以配置哪些字段,默认值,校验之类的。
一个完整的插件目录结构应该像 下面这样:
complete-plugin
├── api.lua
├── deco.lua
├── handler.lua
├── migrations
│ ├── cassandra.lua
│ └── postgres.lua
└── schema.lua
其各个模块的功能如下:
| Module name | Required | Description |
|---|---|---|
| api.lua | No | 插件需要向 Admin API 暴露接口时使用 |
| deco.lua | No | 数据层相关,当插件需要访问数据库时配置 |
| handler.lua | Yes | 插件的主要逻辑,这个将会被 Kong 在不同阶段执行其对应的 handler |
| migrations/*.lua | No | 插件依赖的数据表结构,启用了 deco.lua 时需要定义 |
| schema.lua | Yes | 插件的配置参数定义,主要用于 Kong 参数验证 |
其中 handler.lua 和 schema.lua 是必需的,上 面提到的插件需要暴露出来的方法就定义在 handler.lua 中。
各个阶段
Kong 的插件使用了一个叫 Classic 的 class 机制。所有的插件都是从 base_plugin.lua 基类上继承而来。base_plugin.lua 定义了插件在各个阶段被执行的方法名:
-- 在每个 Nginx 工作进程启动时执行
function BasePlugin:init_worker()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": init_worker")
end
-- 在SSL握手阶段的SSL证书服务阶段执行
function BasePlugin:certificate()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": certificate")
end
-- 从客户端接收作为重写阶段处理程序的每个请求执行。在这个阶段,无论是API还是消费者都没有被识别,因此这个处理器只在插件被配置为全局插件时执行
function BasePlugin:rewrite()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": rewrite")
end
-- 为客户的每一个请求而执行,并在它被代理到上游服务之前执行(路由)
function BasePlugin:access()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": access")
end
-- 从上游服务接收到所有响应头字节时执行
function BasePlugin:header_filter()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": header_filter")
end
-- 从上游服务接收的响应体的每个块时执行。由于响应流回客户端,它可以超过缓冲区大小,因此,如果响应较大,该方法可以被多次调用
function BasePlugin:body_filter()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": body_filter")
end
-- 当最后一个响应字节已经发送到客户端时执行
function BasePlugin:log()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": log")
end
根据方法名也可以看出,这 7 个方法对应于 OpenResty 的不同执行阶段。也就是说插件只能对外暴露出这 7 个方法名中的一个或多个才能被 Kong 的插件机制执行,接下来 Kong 会在 OpenResty 不同的执行阶段,执行插件对应的方法。
逻辑实现
这里以 Kong 自带的 request-termination 插件为例,分析其实现原理。
主核心逻辑
其 handler.lua 的头部是这样定义的:
-- 引入插件基类
local BasePlugin = require "kong.plugins.base_plugin"
local responses = require "kong.tools.responses"
-- 派生出一个子类,其实这里是为了继承来自 Classic 的 __call 元方法,
-- 方便 Kong 在 init 阶段预加载插件的时候执行构造函数 new()
local RequestTerminationHandler = BasePlugin:extend()
-- 设置插件的优先级,Kong 将按照插件的优先级来确定其执行顺序(越大越优先)
-- 需要注意的是应用于 Consumer 的插件因为依赖于 Auth,所以 Auth 类插件优先级普遍比较高
RequestTerminationHandler.PRIORITY = 2
RequestTerminationHandler.VERSION = "0.1.0"
公有方法定义
接下来就是定义插件的公有方法:
-- 插件的构造函数,用于初始化插件的 _name 属性,后面会根据这个属性打印插件名
-- 其实这个方法不是必须的,只是用于插件调试
function RequestTerminationHandler:new()
RequestTerminationHandler.super.new(self, "request-termination")
end
-- 表明需要在 access 阶段执行此插件
function RequestTerminationHandler:access(conf)
-- 执行父类的 access 方法,其实就是为了调试时输出日志用的
RequestTerminationHandler.super.access(self)
-- 接下来的就是插件的主要逻辑,不再赘述
local status_code = conf.status_code
local content_type = conf.content_type
local body = conf.body
local message = conf.message
if body then
ngx.status = status_code
if not content_type then
content_type = "application/json; charset=utf-8";
end
ngx.header["Content-Type"] = content_type
ngx.header["Server"] = server_header
ngx.say(body)
return ngx.exit(status_code)
else
return responses.send(status_code, message)
end
end
return RequestTerminationHandler
参数定义
插件的参数定义在 schema.lua 中,类似于 JSON Schema,主要用于描述插件参数的数据格式:
local Errors = require "kong.dao.errors"
return {
-- 描述插件参数的数据格式,用于 Kong 验证参数
fields = {
status_code = { type = "number", default = 503 },
message = { type = "string" },
content_type = { type = "string" },
body = { type = "string" },
},
-- 自定义更为细粒度的参数校验
self_check = function(schema, plugin_t, dao, is_updating)
if plugin_t.status_code then
if plugin_t.status_code < 100 or plugin_t.status_code > 599 then
return false, Errors.schema("status_code must be between 100 .. 599")
end
end
if plugin_t.message then
if plugin_t.content_type or plugin_t.body then
return false, Errors.schema("message cannot be used with content_type or body")
end
else
if plugin_t.content_type and not plugin_t.body then
return false, Errors.schema("content_type requires a body")
end
end
return true
end
}
当传入的参数无法通过 Kong 的校验时,插件配置将会失败,比如:
# 传入未定义的参数:foo
curl -s -i -X POST \
--url http://localhost:8001/plugins/ \
--data 'name=request-termination' \
--data 'config.foo=bar'
HTTP/1.1 400 Bad Request
Date: Wed, 13 Jun 2018 15:03:39 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.12.3
{"config.foo":"foo is an unknown field"}
# 传入的 status_code 不是合法的数据
curl -s -i -X POST \
--url http://localhost:8001/plugins/ \
--data 'name=request-termination' \
--data 'config.status_code=88'
HTTP/1.1 400 Bad Request
Date: Wed, 13 Jun 2018 15:05:12 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.12.3
{"config":"status_code must be between 100 .. 599"}
deco 加载数据
在 Kong 中,deco.lua 文件是用于加载和初始化数据访问对象(DAO)的脚本。DAO 是用于与底层数据库进行交互的组件,负责处理与数据存储相关的操作。
在 Kong 的架构中,DAOs 是与数据库交互的接口,它们定义了对数据库的查询、插入、更新和删除等操作。deco.lua 脚本负责加载这些 DAO 对象,并将其绑定到 Kong 的核心模块中,使得其他组件可以使用这些 DAOs 来与数据库进行交互。
自定义插件例子
handler.lua
local uuid = require "kong.tools.utils".uuid
local MyUUIDHandler = {}
MyUUIDHandler.PRIORITY = 1
MyUUIDHandler.VERSION = "0.1.0"
function MyUUIDHandler:access(conf)
-- Set header for upstream
local trace_id = kong.request.get_header(conf.header_name)
if not trace_id or trace_id == "" then
-- Generate the header value
trace_id = uuid()
if trace_id then
kong.service.request.set_header(conf.header_name, trace_id)
end
end
kong.ctx.plugin.trace_id = trace_id
end
function MyUUIDHandler:header_filter(conf)
local trace_id = kong.ctx.plugin.trace_id or
kong.request.get_header(conf.header_name)
if not trace_id or trace_id == "" then
trace_id = uuid()
end
kong.response.set_header(conf.header_name, trace_id)
end
return MyUUIDHandler
插件的逻辑只需要在 access 阶段执行就可以
schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "my-uuid",
fields = {
{
consumer = typedefs.no_consumer
},
{
config = {
type = "record",
fields = {
{ header_name = { type = "string", required = true }, },
},
},
},
},
}
插件安装
推荐使用 Lua 模块的包管理器 LuaRocks, 即使我们不需要将包上传到 LuaRocks 官方源也可以使用其来管理插件。
使用 luarocks 安装插件
创建一个 rockspec 文件 kong-plugin-my-uuid-0.1.0-1.rockspec,用来指定包的内容,包含了哪些文件以及版本。
package = "kong-plugin-my-uuid"
version = "0.1.0-1"
local pluginName = package:match("^kong%-plugin%-(.+)$")
supported_platforms = {"linux", "macosx"}
source = {
url = "https://gitxxxxxx.com/xxxx/kong-plugins",
tag = "0.1.0"
}
description = {
summary = "Add uuid in request/response header"
}
dependencies = {}
build = {
type = "builtin",
modules = {
["kong.plugins."..pluginName..".handler"] = pluginName.."/handler.lua",
["kong.plugins."..pluginName..".schema"] = pluginName.."/schema.lua",
}
}
安装这个自定义的插件
luarocks make kong-plugin-my-uuid-0.1.0-1.rockspec
加载插件
vim /etc/kong/kong.conf
将自定义插件的名称添加到 Kong 配置文件中的插件参数中,重新启动 Kong。

官方模板
git clone https://github.com/Kong/kong-plugin.git
目录结构如下:
.
├── LICENSE
├── README.md
├── kong
│ └── plugins
│ └── myplugin
│ ├── handler.lua
│ └── schema.lua
├── kong-plugin-myplugin-0.1.0-1.rockspec
└── spec
└── myplugin
└── 01-access_spec.lua
kong 插件主要有三个文件:
- handler.lua 是包含插件逻辑处理相关代码。
- schema.lua 包含插件的配置文件。
- rockspec 文件是通过 luarock 安装时用的配置文件。
逻辑处理的代码根据 openresty 的不同处理阶段分成了不同的函数,根据插件的功能只需要在不同的函数中添加自己的业务逻辑。